上次分析Android O广播的问题遗留了一个东西没提,那就是官方推荐使用的JobScheduler。这篇就简单了解一下这是个什么东西。
JobScheduler是什么
JobScheduler允许开发者创建在后台执行的job,当预置的条件被满足时,这些Job将会在后台被执行。
在Android开发中,我们会遇到很多这样的情况,比如在未来的某个时间点或者未来满足某种条件(比如插入电源或者连接WiFi)的情况下下去执行一些操作。在Android L上,Google提供了一个叫做JobScheduler的组件来帮助我们处理这种情况。
JobScheduler Api可以在我们的App中执行一些操作,这些操作将会在我们预置的一些条件被满足的时候被执行。和AlarmManager不一样,执行这些操作的时间并不是严格准确的。 JobScheduler会把一系列的job收集起来一起执行,这样既允许我们的job被执行,又能兼顾到手机电量的使用情况,达到节电的目的。
JobScheduler怎么用
JobScheduler的使用非常简单,只需要三步:
- 创建JobService类
- 创建JobInfo,通过builder设定Job的执行选项
- 获取JobScheduler服务执行任务
下面按照这三步放一个简单代码示例。
JobService
JobService的作用是,在JobScheduler监测到系统状态达到对应启动条件时,会启动JobService执行任务。所以我们需要继承JobService创建一个自己的service,然后实现onStartJob和onStopJob这两个方法。
public class JobSchedulerService extends JobService {
private static final int MESSAGE_ID = 100;
private Handler mJobHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
Log.i("WOW", "handle message!!!!");
//请注意,我们手动调用了jobFinished方法。
//当onStartJob返回true的时候,我们必须在合适时机手动调用jobFinished方法
//否则该应用中的其他job将不会被执行
jobFinished((JobParameters) msg.obj, false);
//第一个参数JobParameter来自于onStartJob(JobParameters params)中的params,
// 这也说明了如果我们想要在onStartJob中执行异步操作,必须要保存下来这个JobParameter。
return true;
}
});
// JobService运行在主线程 需要另外开启线程做耗时工作
@Override
public boolean onStartJob(JobParameters params) {
Log.i("WOW", "onStartJob");
// 注意到我们在使用Hanlder的时候把传进来的JobParameters保存下来了
mJobHandler.sendMessage(Message.obtain(mJobHandler, MESSAGE_ID, params));
// 返回false说明job已经完成 不是个耗时的任务
// 返回true说明job在异步执行 需要手动调用jobFinished告诉系统job完成
// 这里我们返回了true,因为我们要做耗时操作。
// 返回true意味着耗时操作花费的事件比onStartJob执行的事件更长
// 并且意味着我们会手动的调用jobFinished方法
return true;
}
@Override
public boolean onStopJob(JobParameters params) {
Log.i("WOW", "onStopJob");
mJobHandler.removeMessages(MESSAGE_ID);
// 当系统收到一个cancel job的请求时,并且这个job仍然在执行(onStartJob返回true),系统就会调用onStopJob方法。
// 但不管是否调用onStopJob,系统只要收到取消请求,都会取消该job
// true 需要重试
// false 不再重试 丢弃job
return false;
}
然后在Manifest文件给service添加一个权限
JobInfo
JobInfo是对任务的描述,比如说需要监听哪些状态、重试策略、任务执行时间、是否持久化等等。 JobInfo.Builder的构造函数需要传入一个jobId,是Job的唯一标志,后续通过该jobId来取消Job。 通过Builder模式构造JobInfo。
//Builder构造方法接收两个参数,第一个参数是jobId,每个app或者说uid下不同的Job,它的jobId必须是不同的
//第二个参数是我们自定义的JobService,系统会回调我们自定义的JobService中的onStartJob和onStopJob方法
JobInfo.Builder builder = new JobInfo.Builder(JOB_ID,
new ComponentName(this, JobSchedulerService.class));
builder.setMinimumLatency(2000) // 2s后执行
.setOverrideDeadline(10000); // 最晚10s后执行
JobScheduler
通过服务获取JobScheduler执行任务即可。
JobScheduler mJobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
int result = mJobScheduler.schedule(builder.build());
if (result <= 0) {
Log.i("WOW", "result is " + result + " Schedule failed");
}
JobScheduler API详解
JobService细节
在前面的代码注释已经有所说明
启动任务之后,会调用onStartJob方法,因为JobService运行在主线程,所以如果在任务开始时,如果要执行耗时的操作,就需要创建一个线程去做。
如果onStartJob执行的是不耗时的任务,就可以返回false,表示任务执行结束。
如果onStartJob起了一个线程执行耗时任务,就要返回true,表示任务还在执行,需要等任务真正结束后手动调用JobFinished()方法告诉系统任务已经结束。
JobInfo细节
JobInfo job=new JobInfo.Builder(i,componentName)
.setMinimumLatency(5000)//最小延时 5秒
.setOverrideDeadline(60000)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)//任意网络
.setMinimumLatency(5000)//5秒 最小延时、
.setOverrideDeadline(60000)//maximum最多执行时间
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)//免费的网络---wifi 蓝牙 USB
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)//任意网络---
/**
设置重试/退避策略,当一个任务调度失败的时候执行什么样的测量采取重试。
initialBackoffMillis:第一次尝试重试的等待时间间隔ms
*backoffPolicy:对应的退避策略。比如等待的间隔呈指数增长。
*/
.setBackoffCriteria(long initialBackoffMillis, int backoffPolicy)
.setBackoffCriteria(JobInfo.MAX_BACKOFF_DELAY_MILLIS, JobInfo.BACKOFF_POLICY_LINEAR)
.setPeriodic (long intervalMillis)//设置执行周期,每隔一段时间间隔任务最多可以执行一次。
.setPeriodic(long intervalMillis,long flexMillis)//在周期执行的末端有一个flexMiliis长度的窗口期,任务就可以在这个窗口期执行。
//设置设备重启后,这个任务是否还要保留。需要权限: RECEIVE_BOOT_COMPLETED
.setPersisted(boolean isPersisted);
.setRequiresCharging(boolean )//是否需要充电
.setRequiresDeviceIdle(boolean)//是否需要等设备出于空闲状态的时候
.addTriggerContentUri(uri)//监听uri对应的数据发生改变,就会触发任务的执行。
.setTriggerContentMaxDelay(long duration)//设置Content发生变化一直到任务被执行中间的最大延迟时间
//设置Content发生变化一直到任务被执行中间的延迟。如果在这个延迟时间内content发生了改变,延迟时间会重写计算。
.setTriggerContentUpdateDelay(long durationMilimms)
需要注意的是
setRequiredNetworkType(int networkType),setRequiresCharging(boolean requireCharging),setRequiresDeviceIdle(boolean requireIdle)
这几个方法可能会使得你的任务无法执行,除非调用setOverrideDeadline(long time)设置了最大延迟时间,使得你的任务在为满足条件的情况下也会被执行。
setMinimumLatency(long minLatencyMillis):这个方法指定我们的Job至少要多少毫秒之后执行,比如setMinimumLatency(5000),就表明我们这是了这个JobScheduler之后,这个Job至少要5秒之后执行,前五秒肯定是不会执行的。这个参数和setPeriodic互斥。两个同时设置会抛出异常。
setRequiredNetworkType(int networkType):来启动我们这个Job时所需要的网络类型,一共有三个值JobInfo.NETWORK_TYPE_NONE表明启动我们这个Job时不需要任何的网络连接;JobInfo.NETWORK_TYPE_ANY表明启动我们这个Job时只要连着网就可以,不要求网络类型。JobInfo.NETWORK_TYPE_UNMETERED表明启动我们这个Job时需要连接Wifi.
Android O 对JobScheduler的改进
您现在可以将工作队列与计划作业关联。要将一个工作项添加到作业的队列中,请调用
JobScheduler.enqueue())。当作业运行时,它可以将待定工作从队列中剥离并进行处理。这种功能可以处理之前需要启动后台服务(尤其是实现IntentService的服务)的许多用例。您现在可以通过调用
JobInfo.Builder.setClipData()) 的方式将ClipData与作业关联。利用此选项,您可以将 URI 权限授予与作业关联,类似于这些权限传递到Context.startService()的方式。您也可以将 URI 权限授予用于工作队列上的 intent。计划作业现在支持多个新的约束条件:
JobInfo.isRequireStorageNotLow())如果设备的可用存储空间非常低,作业将不会运行。
JobInfo.isRequireBatteryNotLow())如果电池电量等于或低于临界阈值,作业将不会运行;临界阈值是指设备显示 Low battery warning 系统对话框的电量。
-
作业需要一个按流量计费的网络连接,比如大多数移动数据网络数据套餐。
源码分析
JobSchedulerService启动
首先因为知道JobScheduler是通过系统服务拿到的:
(JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
所以可以想到,Android启动时所有的系统服务都是在SystemServer里启动:
//frameworks/base/services/java/com/android/server/SystemServer.java
mSystemServiceManager.startService(JobSchedulerService.class);
于是代码进入JobSchedulerService.java
//frameworks/base/services/core/java/com/android/server/job/JobSchedulerService.java
public final class JobSchedulerService extends com.android.server.SystemService
implements StateChangedListener, JobCompletedListener {
官方代码注释里有一句说明:
The JobSchedulerService knows nothing about constraints, or the state of active jobs. It receives callbacks from the various controllers and completed jobs and operates accordingly.
就是JobSchedulerService对Job的状态和约束都不了解,完全是通过各种controller的回调去处理各种Job。
然后我们看其构造函数
public JobSchedulerService(Context context) {
super(context);
// 先创建在主线程的JobHandler
mHandler = new JobHandler(context.getMainLooper());
mConstants = new Constants(mHandler);
// binder服务端
mJobSchedulerStub = new JobSchedulerStub();
mJobs = JobStore.initAndGet(this);
// Create the controllers.
mControllers = new ArrayList();
mControllers.add(ConnectivityController.get(this));
mControllers.add(TimeController.get(this));
mControllers.add(IdleController.get(this));
mControllers.add(BatteryController.get(this));
mControllers.add(AppIdleController.get(this));
mControllers.add(ContentObserverController.get(this));
mControllers.add(DeviceIdleJobsController.get(this));
}
JobHandler
是一个在主线程运行的Handler,主要处理四个消息。
private class JobHandler extends Handler {
public JobHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message message) {
synchronized (mLock) {
if (!mReadyToRock) {
return;
}
}
switch (message.what) {
case MSG_JOB_EXPIRED:
...
break;
case MSG_CHECK_JOB:
...
break;
case MSG_CHECK_JOB_GREEDY:
...
break;
case MSG_STOP_JOB:
...
break;
}
maybeRunPendingJobsH();
// Don't remove JOB_EXPIRED in case one came along while processing the queue.
removeMessages(MSG_CHECK_JOB);
}
...
}
Constants
这里面定义了一些与系统全局设置保持同步的常量。 这一类或其任何域的访问应该同时持有JobSchedulerService.mLock锁来完成。
private final class Constants extends ContentObserver {}
JobSchedulerStub
JobSchedulerStub作为实现接口IJobScheduler的binder服务端。
final class JobSchedulerStub extends IJobScheduler.Stub {}
JobStore.initAndGet
这个方法是创建了一个JobStore的单例由JobSchedulerService使用。
// frameworks/base/services/core/java/com/android/server/job/JobStore.java
/** Used by the {@link JobSchedulerService} to instantiate the JobStore. */
static JobStore initAndGet(JobSchedulerService jobManagerService) {
synchronized (sSingletonLock) {
if (sSingleton == null) {
sSingleton = new JobStore(jobManagerService.getContext(),
jobManagerService.getLock(), Environment.getDataDirectory());
}
return sSingleton;
}
}
JobStore
该类的作用是维护作业计划程序正在跟踪的作业主列表。 这些作业通过引用进行比较,因此此类中的任何函数都不应复制。 还处理持久作业的读/写。
创建一个JobStore实例,进行从磁盘读取文件。该方法会创建job目录以及jobs.xml文件, 以及从文件中读取所有的JobStatus。
private JobStore(Context context, Object lock, File dataDir) {
mLock = lock;
mContext = context;
mDirtyOperations = 0;
File systemDir = new File(dataDir, "system");
File jobDir = new File(systemDir, "job");
jobDir.mkdirs();
// 创建/data/system/job/jobs.xml
mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"));
mJobSet = new JobSet();
readJobMapFromDisk(mJobSet);
}
readJobMapFromDisk方法从磁盘读取Job信息,先看一下Job.xml文件结构
可以看出这个xml中主要记录了每一个Job的jobid, JobService的名字,包名,以及触发该Job的一些条件信息。
对于xml解析就不分析了,这个思路都一样的,就是流程上是从xml中读取job的信息,然后利用这些信息创建JobStatus, JobStatus对象包含了JobInfo信息(Jobid,package,class),还有该Job的delay,deadline信息,用于schedule。JobStatus添加到mJobSet。
StateController
构造函数还创建了7个StateController:
| 类型 | 说明 |
|---|---|
| ConnectivityController | 注册监听网络连接状态的广播 |
| TimeController | 注册监听job时间到期的广播 |
| IdleController | 注册监听屏幕亮/灭,dream进入/退出,状态改变的广播 |
| BatteryController | 注册监听电池是否充电,电量状态的广播 |
| AppIdleController | 监听app是否空闲 |
| ContentObserverController | 通过ContentObserver监测content URIs的变化 |
| DeviceIdleJobsController | 根据doze状态为app设置约束。 |
前面提到过,JobSchedulerService是根据这些controller的回调处理Job的,所以简单看一下ConnectivityController
public interface StateChangedListener {
public void onControllerStateChanged();
public void onRunJobNow(JobStatus jobStatus);
public void onDeviceIdleStateChanged(boolean deviceIdle);
}
public abstract class StateController {
protected static final boolean DEBUG = JobSchedulerService.DEBUG;
protected final Context mContext;
protected final Object mLock;
protected final StateChangedListener mStateChangedListener;
public StateController(StateChangedListener stateChangedListener, Context context,
Object lock) {
mStateChangedListener = stateChangedListener;
mContext = context;
mLock = lock;
}
public abstract void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob);
public void prepareForExecutionLocked(JobStatus jobStatus) {}
public abstract void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, boolean forUpdate);
public void rescheduleForFailure(JobStatus newJob, JobStatus failureToReschedule) {}
public abstract void dumpControllerStateLocked(PrintWriter pw, int filterUid);
}
public class ConnectivityController extends StateController implements ConnectivityManager.OnNetworkActiveListener {
public static ConnectivityController get(JobSchedulerService jms) {
synchronized (sCreationLock) {
if (mSingleton == null) {
//单例模式
mSingleton = new ConnectivityController(jms, jms.getContext());
}
return mSingleton;
}
}
private ConnectivityController(StateChangedListener stateChangedListener, Context context) {
super(stateChangedListener, context);
//注册监听网络连接状态的广播,且采用BackgroundThread线程
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
mContext.registerReceiverAsUser(
mConnectivityChangedReceiver, UserHandle.ALL, intentFilter, null,
BackgroundThread.getHandler());
ConnectivityService cs =
(ConnectivityService)ServiceManager.getService(Context.CONNECTIVITY_SERVICE);
if (cs != null) {
if (cs.getActiveNetworkInfo() != null) {
mNetworkConnected = cs.getActiveNetworkInfo().isConnected();
}
mNetworkUnmetered = mNetworkConnected && !cs.isActiveNetworkMetered();
}
}
}
所以可以知道,Android O以后禁止了一些广播的发送后,都是由这些Controller进行动态注册广播,由这些controller转交给JobScheduler进行处理。
流程控制
对遗留问题的说明
所以很明显,Android Framework对JobInfo已经设计好一些状态处理,比如说网络变化。所以这样不再用广播吊起更多App而引起性能问题了。
Ref